2025-08-10 객체지향 정글 스터디 - ch01, ch02

과제: 그래서 객체가 도대체 무엇인가?

객체지향 문맥에서 객체란, 문제를 해결하는 데 필요한 지식만 남긴 기계라고 말할 수 있을 것 같다. 하나의 문장에 꾹꾹 눌러담아 풀어서 설명할 필요가 있다.

  1. 문제를 해결: 우리가 컴퓨터 또는 스마트폰을 사용하는 이유는 문제를 해결하기 위함이다. 친구이게 메시지를 보내거나 자료를 정리하는 등의 모든 과정에서 우리는 도구 없이는 일을 하지 못할 정도가 되었다. 따라서, 객체는 문제를 해결하는 도구이다.
  2. 필요한 지식:
  3. 기계:

협력이란, 믿음이다.

p.56 그림 2.5 "메시지를 통한 앨리스와 음료 사이의 협력 관계"를 도식화해보자면 아래와 같다:

graph
  Client -- drinkBeverage() --> 앨리스
  앨리스 -- drunken(quantity) --> 음료

앨리스는 Client(프로세스를 최초로 시작한 누군가)에 의해 drinkBeverage 메시지를 수신받아 자기만의 행동을 수행하기 시작한다. 영어를 조금 공부한 한국인인 앨리스는 drinkBeverage라는 말의 의미가 "음료를 마셔라" 라고 알고 있기 때문에 마실 음료의 대상을 찾기 시작한다. "내가 마실 수 있는 음료가 있을까?"

객체는 협력할 대상을 자율적으로 선택할 수 있다. 사실 앨리스는 마실 수 있는 음료가 없을 수도 있다. 그러면 적당히 Client에게 에러를 던지면 된다. 앨리스는 마실 수 있는 음료가 여러가지일 수도 있다. 그 중에 적합하다고 생각하는 음료 하나를 선택하기만 하면 된다.

마침 앨리스는 음료라는 객체와 친분이 있다. 음료는 앨리스의 믿음직한 동료로써, 정확히 언제인지는 모르지만 앨리스가 태어날때부터, 혹은 살아생전에 알게 된 사이이다. 앨리스는 항상 목이 마르기 때문에 drunken이라는 메시지를 전송할 대상을 찾고 있었고, 마침 음료가 적임자로 나타났던 것이다.

책에서 설명한 링크라는 것은 사실 의존성이다. 의존성은 객체가 다른 객체를 알고 있거나 소유하거나 같은 가족이거나 하는 등의 관계를 뜻한다. 생성자 타임에 주입을 받을 수도 있고, 필요한 때에 객체를 생성하여 가지고 있을 수도 있고, 심지어 다른 객체로부터 프로퍼티를 상속받아 상위 타입의 흉내를 낼 수도 있다.

음료는 앨리스의 요구사항을 완벽하게 충족시켜주었다. 음료가 무슨 짓을 하는지는 잘 모르겠지만, 앨리스는 어찌됐건 drunken이라는 메시지를 전송하면 음료는 확실하게 그 메시지를 이해하고 스스로 상태를 바꿀 것임을 굳게 믿기 때문이다. 나중에라도 앨리스가 궁금해서 남은 음료의 양을 물어보게 되더라도 앨리스는 이미 자기가 마신 만큼 줄어든 음료의 양을 기대하게 될 것이다.

앨리스와 음료는 링크를 기준으로 서로 계약을 맺은 거나 다름없다. 만약 음료가 배신을 하고 drunken 이라는 메시지에 아무 반응도 하지 않거나 반대로 본인의 상태를 늘려버린다면 어떻게 될까? 앨리스는 억지로 음료의 남은 양을 컨트롤 할 수 없다. 다만 앨리스의 요구사항을 충족시켜주지 못하였으므로, 둘 간의 관계는 계약위반이라고 볼 수 있다.

계약을 잘 지키는 것은 현실세계에서나 객체들의 세계에서나 중요한 일이다. 앨리스는 음료에게 drunken이라는 메시지 말고도 getRemain이라는 메시지를 전달하여 남은 음료가 얼마나 있는지를 요구하는 시나리오를 상상해볼 수 있다. 만약 음료가 태업을 하여 getRemain이라는 메시지에 똑바로 답하지 못한다면 우리는 음료의 코드를 고쳐야 할까? 그래도 되지만 앨리스는 차라리 다른 음료와 계약을 맺기를 희망할 것이다. 세상에 drunkengetRemain이라는 메시지를 이해하는 다른 음료는 얼마든지 구하면 되기 때문이다.

믿음을 저버린 음료는 더 이상 앨리스와 협력하지 못한다. 반대로 믿음이 있기 때문에 앨리스는 음료가 어떤 방식으로 행동하는지 관심을 갖지 않아도 된다. 다음은 내가 생각하는 믿을만한 음료들의 사례들을 가지고 왔다.

class Cola {
  private _remain: number = 100;
  public drunken(quantity: number) {
    if (this._remain < quantity) {
	  throw new Error(`${quantity} 만큼 마실 수 없습니다.`);
    }
    this._remain -= quantity;
  }
  public getRemain(): number {
    return this._remain;
  }
}

아주 정석적인 케이스이다. 하지만 아래와 같이 작성해도 앨리스는 만족할 것이다:

class Fanta {
  private readonly _initial: number = 100;
  private readonly _drunkHistory: number[] = [];
  public drunken(quantity: number) {
    if (this.getRemain() < quantity) {
	  throw new Error(`${quantity} 만큼 마실 수 없습니다.`);
    }
	this._drunkHistory.push(quantity);
  }
  public getRemain(): number {
    let sum = 0;
	for (const q of this._drunkHistory) {
	  sum += q;
	}
    return this._initial - sum;
  }
}

Cola나, Fanta나 행동방식이던 상태던 완전히 다른 코드를 가지고 있으나 앨리스가 보기에 이 둘은 완전히 동등하며, 둘 다 계약을 만족시킨다.

계약 관점에서 Cola와 Fanta는 협력할 수 있는 믿음직스러운 객체로 보인다.

example code

직접 실행하여 결과를 확인해보고 싶은 분들은 아래 코드를 마저 작성한 다음 콘솔 결과가 일치하는지 확인해보라:

const beverage1 = new Cola();
const beverage2 = new Fanta();

const commands = [1, 7, 23];

// beverage1 case
for (const cmd of commands) {
  beverage1.drunken(cmd);
  console.log(beverage1.getRemain());
}

// beverage2 case
for (const cmd of commands) {
  beverage2.drunken(cmd);
  console.log(beverage2.getRemain());
}

Q&A

객체지향적 타입과 타입별 동작을 매핑하는 걸 다른 객체로 미룬건가요, 아니면 실제로 매핑할 필요가 없는건가요?

@최재원 스레드 참조 재원님의 블로그에 올려주신